const randomString = require('randomstring');
const path = require('path');
const { cfgNodeKind } = require('./../../functions');
const utility = require('../../utility');

function makeMatrix(rows, columns) {
  return Array.from({ length: rows }, () => Array.from({ length: columns }, () => '-'));
}

function testIdsCheck(coveredTestIds, testIdGroups) {
  if (!testIdGroups) {
    testIdGroups = [];
  }
  if (!coveredTestIds) {
    return Array.from({ length: testIdGroups.length + 1 }, () => false);
  }
  const covered = [];
  testIdGroups.forEach((testIds, index) => {
    covered.push(false);
    testIds.forEach((testId) => {
      if (coveredTestIds[testId]) {
        covered[index] = true;
      }
    });
  });
  covered.push(Object.keys(coveredTestIds).length > 0);
  return covered;
}

function calcStatemetsBlockInFunction(func, linesInSourceFile, testIdGroups) {
  if (!func.functionCfg) {
    return [];
  }
  const lines = {};
  let beginLine;
  let endLine;
  if (func.functionCfg.location) {
    beginLine = func.functionCfg.location.line;
  } else if (func.location) {
    beginLine = func.location.line;
  } else {
    return [];
  }
  endLine = beginLine;

  Object.values(func.functionCfg.nodes).forEach(({ end }) => {
    if (end.line > endLine) {
      endLine = end.line;
    }
  });

  for (let i = beginLine; i <= endLine; i += 1) {
    lines[i] = {
      line: i, content: linesInSourceFile[i - 1], isData: true,
    };
  }

  Object.values(func.functionCfg.nodes).forEach(({
    begin,
    end,
    statements,
  }) => {
    if (!statements) {
      statements = { 0: { begin, end, testIds: { 1: 1 } } };
    }
    Object.values(statements).forEach(({ begin, end, testIds }) => {
      const sign = testIdsCheck(testIds, testIdGroups);
      const curSign = lines[begin.line].sign;
      if (curSign === undefined) {
        lines[begin.line].sign = sign;
      } else {
        lines[begin.line].sign = lines[begin.line].sign.map((s, idx) => {
          if (s === true) {
            return sign[idx];
          }
          return false;
        });
      }
      // 如果该token跨越了多行
      if (end.line > begin.line) {
        for (let i = begin.line + 1; i <= end.line; i += 1) {
          const curSign = lines[i].sign;
          if (curSign === undefined) {
            lines[i].sign = sign;
          } else {
            lines[i].sign = lines[i].sign.map((s, idx) => {
              if (s === true) {
                return sign[idx];
              }
              return false;
            });
          }
        }
      }
    });
  });

  return Object.entries(lines)
    .sort((a, b) => a[0] < b[0])
    .map((a) => a[1]);
}

/**
 * This function is not pure, it will change the first argument 'functions'
 */
function generateStatemetBlocks(functions, linesInSourceFile, testIdGroups) {
  functions.forEach((func) => {
    const lines = calcStatemetsBlockInFunction(func, linesInSourceFile, testIdGroups);
    return Object.assign(
      func,
      {
        statementsBlocks: {
          // title: '语句', coverage: calcCoverageStrOfLines(lines), blocks: [{ lines }],
          title: '语句', coverage: `${func.statementCovered} / ${func.statementTotal}`, blocks: [{ lines }],
        },
      }
    );
  });
  // functions.map((func) => console.log('> statements\n', JSON.stringify(func.statementsBlocks)));
  return { functions, linesInSourceFile };
}

function calcBranchBlockInFunction(func, linesInSourceFile, testIdGroups) {
  const lines = [];

  if (!func.functionCfg || !func.functionCfg.branches || Number(func.branchTotal) === 0) {
    return [];
  }

  Object.values(func.functionCfg.branches).reverse().forEach(({ nodeId, branches }) => {
    const { begin, end } = func.functionCfg.nodes[nodeId];
    const { line: l1, column: s } = begin;
    const { line: l2, column: e } = end;

    const tmpLines = Object.keys(branches).reverse().map((nodeId) => {
      const line = func.functionCfg.nodes[nodeId] && func.functionCfg.nodes[nodeId].begin
        ? func.functionCfg.nodes[nodeId].begin.line
        : '';
      const { kind, value, testIds } = branches[nodeId];
      const sign = testIdsCheck(testIds, testIdGroups);
      const isData = true;
      switch (kind) {
        case 'BOOLEAN':
          return {
            content: Number(value) !== 0 ? 'T' : 'F',
            sign,
            isData,
          };
        case 'VALUE':
          return {
            line,
            content: `case ${value}`,
            sign,
            isData,
          };
        case 'DEFAULT':
          return {
            line,
            content: 'default',
            sign,
            isData,
          };
        default:
          return {
            content: 'invalid',
            sign,
            isData,
          };
      }
    });
    for (let i = l1; i <= l2; i += 1) {
      if (linesInSourceFile[i - 1] === undefined) {
        // eslint-disable-next-line no-continue
        continue;
      }
      lines.push({
        line: l1 === i ? i : undefined,
        content: linesInSourceFile[i - 1].slice(i === l1 ? s - 1 : 0, i === l2 ? e - 1 : linesInSourceFile[i - 1].length),
      });
    }
    tmpLines.map((l) => lines.push(l));
  });
  return lines;
}

/**
 * This function is not pure, it will change the argument 'functions'
 */
function generateBranchBlocks(functions, linesInSourceFile, testIdGroups) {
  functions.forEach((func) => {
    const lines = calcBranchBlockInFunction(func, linesInSourceFile, testIdGroups);
    return Object.assign(
      func,
      {
        branchBlocks: {
          // title: '分支', coverage: calcCoverageStrOfLines(lines), blocks: [{ lines }],
          title: '分支', coverage: `${func.branchCovered} / ${func.branchTotal}`, blocks: [{ lines }],
        },
      }
    );
  });
  // functions.map((func) => console.log('> branches\n', JSON.stringify(func.branchBlocks)));
  return functions;
}

function calcDecisionBlockInFunction(func, linesInSourceFile, testIdGroups) {
  const lines = [];

  if (!func.functionCfg || Number(func.decisionTotal) === 0) {
    return [];
  }

  if (func.functionCfg.decisions) {
    Object.values(func.functionCfg.decisions).reverse().forEach(({ expr, coveredConditions, begin }) => {
      let trueCovered = [testIdsCheck({}, testIdGroups)];
      let falseCovered = [testIdsCheck({}, testIdGroups)];
      if (coveredConditions) {
        Object.keys(coveredConditions).forEach((truthTable) => {
          const { testIds } = coveredConditions[truthTable];
          if (truthTable[truthTable.length - 1] === '1') {
            trueCovered.push(testIdsCheck(testIds, testIdGroups));
          }
          if (truthTable[truthTable.length - 1] === '0') {
            falseCovered.push(testIdsCheck(testIds, testIdGroups));
          }
        });
      }
      trueCovered = trueCovered.reduce((pre, item) => item.map((subItem, idx) => (pre[idx] ? pre[idx] : false) || item[idx]), []);
      falseCovered = falseCovered.reduce((pre, item) => item.map((subItem, idx) => (pre[idx] ? pre[idx] : false) || item[idx]), []);
      const groupLines = [];
      groupLines.push({ line: begin.line, content: expr });
      groupLines.push({ content: 'T', sign: trueCovered });
      groupLines.push({ content: 'F', sign: falseCovered });
      lines.push({ l: begin.line, groupLines });
    });
  }
  if (func.functionCfg.branches) {
    Object.values(func.functionCfg.branches).reverse().forEach((branch) => {
      if (!func.functionCfg.nodes[branch.nodeId] || func.functionCfg.nodes[branch.nodeId].kind !== cfgNodeKind.conditionSwitch) {
        return;
      }
      const groupLines = [];
      const { begin, end } = func.functionCfg.nodes[branch.nodeId];
      const { line: l1, column: s } = begin;
      const { line: l2, column: e } = end;
      for (let i = l1; i <= l2; i += 1) {
        if (linesInSourceFile[i - 1] === undefined) {
          // eslint-disable-next-line no-continue
          continue;
        }
        groupLines.push({
          line: l1 === i ? i : undefined,
          content: linesInSourceFile[i - 1].slice(i === l1 ? s - 1 : 0, i === l2 ? e - 1 : linesInSourceFile[i - 1].length),
        });
      }
      Object.keys(branch.branches).reverse().forEach((nodeId) => {
        const line = func.functionCfg.nodes[nodeId] && func.functionCfg.nodes[nodeId].begin
          ? func.functionCfg.nodes[nodeId].begin.line
          : '';
        const { kind, value, testIds } = branch.branches[nodeId];
        groupLines.push({
          line,
          content: kind.toLowerCase() === 'default' ? 'default' : `case ${value}`,
          sign: testIdsCheck(testIds, testIdGroups),
        });
      });
      lines.push({ l: l1, groupLines });
    });
  }
  return lines.sort((a, b) => a.l - b.l).reduce((pre, { groupLines }) => {
    groupLines.forEach((item) => {
      pre.push(item);
    });
    return pre;
  }, []);
}

function generateDecisionBlocks(functions, linesInSourceFile, testIdGroups) {
  functions.forEach((func) => {
    const lines = calcDecisionBlockInFunction(func, linesInSourceFile, testIdGroups);
    return Object.assign(
      func,
      {
        decisionBlocks: {
          title: '决策', coverage: `${func.decisionCovered} / ${func.decisionTotal}`, blocks: [{ lines }],
        },
      }
    );
  });
  return functions;
}

function calcConditionBlockInFunction(func, testIdGroups) {
  const lines = [];

  if (!func.functionCfg || !func.functionCfg.decisions || Number(func.decisionTotal) === 0) {
    return [];
  }

  Object.values(func.functionCfg.decisions).reverse().forEach((decision) => {
    if (!decision.conditions) {
      return;
    }
    Object.keys(decision.conditions).reverse().forEach((conditionId) => {
      let trueCovered = [testIdsCheck({}, testIdGroups)];
      let falseCovered = [testIdsCheck({}, testIdGroups)];
      if (decision.coveredConditions) {
        Object.keys(decision.coveredConditions).forEach((truthTable) => {
          const { testIds } = decision.coveredConditions[truthTable];
          if (truthTable[conditionId] === '1') {
            trueCovered.push(testIdsCheck(testIds, testIdGroups));
          }
          if (truthTable[conditionId] === '0') {
            falseCovered.push(testIdsCheck(testIds, testIdGroups));
          }
        });
      }
      lines.push({
        line: decision.conditionsInfo ? decision.conditionsInfo[conditionId].begin.line : '',
        content: decision.conditions[conditionId],
      });
      trueCovered = trueCovered.reduce((pre, item) => item.map((subItem, idx) => (pre[idx] ? pre[idx] : false) || item[idx]), []);
      falseCovered = falseCovered.reduce((pre, item) => item.map((subItem, idx) => (pre[idx] ? pre[idx] : false) || item[idx]), []);
      lines.push({ content: 'T', sign: trueCovered });
      lines.push({ content: 'F', sign: falseCovered });
    });
  });
  return lines;
}

function generateConditionBlocks(functions, testIdGroups) {
  functions.forEach((func) => {
    const lines = calcConditionBlockInFunction(func, testIdGroups);
    return Object.assign(
      func,
      {
        conditionsBlocks: {
          title: '条件', coverage: `${func.conditionCovered} / ${func.conditionTotal}`, blocks: [{ lines }],
        },
      }
    );
  });
  return functions;
}

function calcCondMcDcBlockInFunction(func, testIdGroups) {
  const blocks = [];

  if (!func.functionCfg || !func.functionCfg.decisions || Number(func.mcdcTotal) === 0) {
    return [];
  }
  Object.keys(func.functionCfg.decisions).reverse().forEach((decisionId) => {
    const {
      expr, conditions, begin, conditionCoveredPairs, cases,
    } = func.functionCfg.decisions[decisionId];
    if (Object.keys(conditions).length < 2) {
      return;
    }
    const lines1 = [];
    const lines2 = [];
    const lines3 = [];
    lines1.push({ content: expr, line: begin.line });

    // make the conditions ordered by the key of conditions
    const condPairs = Object.entries(conditions).sort((pair1, pair2) => pair2[0] - pair1[0]);
    const atoms = condPairs.map((pair, idx) => ({
      id: pair[0],
      name: pair[1],
      atomName: `Atom${idx + 1}`,
    }));

    atoms.forEach(({ name, atomName }) => lines1.push({ content: `${atomName}:  ${name}` }));
    // When replacing, alway replace the longest first
    const sortedAtomes = [...atoms].sort((a, b) => b.name.length - a.name.length);
    const replacedExpr = sortedAtomes.reduce((result, { name, atomName }) => result.replace(name, atomName), expr);
    lines1.push({ content: replacedExpr });

    lines2.push({ content: `${atoms.map(({ atomName }) => atomName).join('   ')}   COMBINED` });

    if (cases) {
      Object.keys(cases).forEach((caseId) => {
        const row = [];
        [...atoms, { id: Object.keys(conditions).length }].forEach(({ id }) => {
          if (cases[caseId].condKey[Number(id)] === '0') {
            return row.push('F');
          }
          if (cases[caseId].condKey[Number(id)] === '1') {
            return row.push('T');
          }
          return row.push('-');
        });
        // eslint-disable-next-line no-plusplus
        lines2.push({ line: Number(caseId) + 1, content: row.join('       '), sign: testIdsCheck(cases[caseId].testIds, testIdGroups) });
      });
      atoms.forEach(({ id, atomName }) => {
        let covered = false;
        if (!conditionCoveredPairs[id]) {
          conditionCoveredPairs[id] = [];
        }
        conditionCoveredPairs[id].forEach(([i, j]) => {
          if (covered) {
            return;
          }
          if (cases[i].testIds && Object.keys(cases[i].testIds).length > 0
            && cases[j].testIds && Object.keys(cases[j].testIds).length > 0
          ) {
            covered = true;
          }
        });
        covered = [covered];
        if (testIdGroups) {
          testIdGroups.forEach(() => {
            covered.unshift(null);
          });
        }
        lines3.push({ content: `${atomName}: ${conditionCoveredPairs[id].map(([i, j]) => `(${i + 1}, ${j + 1})`).join(',')}`, sign: covered });
      });
    }
    blocks.push({ lines: lines1 }, { lines: lines2 }, { lines: lines3 });
  });

  return blocks;
}

/**
 * This function is not pure, it will change the argument 'functions'
 */
function generateCondMcDcBlocks(functions, testIdGroups) {
  functions.forEach((func) => {
    const blocks = calcCondMcDcBlockInFunction(func, testIdGroups);
    return Object.assign(
      func,
      {
        // mcdcBlocks: { title: 'MC/DC', coverage: calcCoverageStrOfBlocks(blocks), blocks },
        condMcdcBlocks: { title: 'MC/DC', coverage: `${func.mcdcCovered} / ${func.mcdcTotal}`, blocks },
      }
    );
  });
  // functions.map((func) => console.log('> MC/DC\n', JSON.stringify(func.mcdcBlocks)));
  return functions;
}

function calcMcDcBlockInFunction(func, testIdGroups) {
  const blocks = [];

  if (!func.functionCfg || !func.functionCfg.decisions || Number(func.mcdcTotal) === 0) {
    return [];
  }
  Object.keys(func.functionCfg.decisions).reverse().forEach((decisionId) => {
    const {
      expr, conditions, cases, begin,
    } = func.functionCfg.decisions[decisionId];
    const lines1 = [];
    const lines2 = [];
    lines1.push({ content: expr, line: begin.line });

    // make the conditions ordered by the key of conditions
    const condPairs = Object.entries(conditions).sort((pair1, pair2) => pair2[0] - pair1[0]);

    const condIdToAtomIdMap = Object.fromEntries(condPairs.map((pair, idx) => [pair[0], idx]));
    const atoms = condPairs.map((pair, idx) => [`Atom${idx + 1}`, pair[1]]);

    atoms.forEach(([name, content]) => lines1.push({ content: `${name}:  ${content}` }));

    // When replacing, alway replace the longest first
    atoms.sort((a, b) => b[1].length - a[1].length);

    const replacedExpr = atoms.reduce((result, [name, str]) => result.replace(str, name), expr);
    lines1.push({ content: replacedExpr });

    const names = atoms.map((pair) => pair[0]).sort();
    lines2.push({ content: `${names.join('   ')}   COMBINED` });

    const columnNumber = Object.keys(conditions).length + 1;
    const rows = Object.keys(cases).length;
    const buf = makeMatrix(rows, columnNumber);

    const signList = Array.from({ length: rows });

    Object.entries(cases).forEach(([key, v]) => {
      Object.values(v.conditions).forEach(({ id, value }) => {
        buf[key][condIdToAtomIdMap[id]] = value ? 'T' : 'F';
      });
      signList[key] = testIdsCheck(v.testIds, testIdGroups);
      buf[key][columnNumber - 1] = v.return ? 'T' : 'F';
    });

    buf.forEach((row, idx) => {
      lines2.push({ content: `  ${row.join('       ')}`, sign: signList[idx], isData: true });
    });

    blocks.push({ lines: lines1 }, { lines: lines2 });
  });

  return blocks;
}

/**
 * This function is not pure, it will change the argument 'functions'
 */
function generateMcDcBlocks(functions, testIdGroups) {
  functions.forEach((func) => {
    const blocks = calcMcDcBlockInFunction(func, testIdGroups);
    return Object.assign(
      func,
      {
        // mcdcBlocks: { title: 'MC/DC', coverage: calcCoverageStrOfBlocks(blocks), blocks },
        mcdcBlocks: { title: 'MC/DC', coverage: `${func.mcdcCovered} / ${func.mcdcTotal}`, blocks },
      }
    );
  });
  // functions.map((func) => console.log('> MC/DC\n', JSON.stringify(func.mcdcBlocks)));
  return functions;
}

async function generateInvokeImage(rootFunc, relations, depth, testData = undefined, outputDir) {
  relations = relations.filter((r) => r.depth.minDepth <= depth); // 只展示n层
  const funcId = rootFunc._id.toString();
  const funcMap = new Map();
  relations.forEach((r) => {
    const key = r.fromFuncId.toString();
    if (!funcMap.get(key)) {
      funcMap.set(key, {
        functionId: r.fromFuncId,
        children: new Map(),
      });
    }
    const funcFrom = funcMap.get(key);
    if (r.functionId.toString() === funcFrom.functionId.toString()) {
      // 入口函数
      funcFrom.functionName = rootFunc.functionName;
    }
    if (r.toFuncId) {
      // 已定义
      if (!funcMap.get(r.toFuncId.toString())) {
        funcMap.set(r.toFuncId.toString(), {
          functionId: r.toFuncId,
          children: new Map(),
        });
      }
      const funcTo = funcMap.get(r.toFuncId.toString());
      funcTo.functionName = r.toFuncName;
      funcFrom.children.set(r.toFuncName, {
        toFunc: funcTo,
        enableStub: r.enableStub,
        covered: testData ? !!(r.testCases
          && r.testCases.map((value) => String(value)).indexOf(testData._id.toString()) > -1) : !!(r.testCases && r.testCases.length),
      });
    } else {
      // 未定义
      funcFrom.children.set(r.toFuncName, {
        toFunc: {
          functionName: r.toFuncName,
          mangledId: r.toFuncMangledId,
        },
        enableStub: r.enableStub,
        covered: testData ? !!(r.testCases
          && r.testCases.map((value) => String(value)).indexOf(testData._id.toString()) > -1) : !!(r.testCases && r.testCases.length),
      });
    }
  });

  const root = funcMap.get(funcId);
  const gray = 'rgb(164, 177, 191)';
  const green = 'rgb(27, 173, 39)';
  const blue = 'rgb(59, 109, 242)';
  const fontblack = 'rgb(102, 102, 102)';
  const fontwhite = 'white';
  const graybg = 'rgb(219, 219, 219)';
  const whitebg = 'white';

  // 将map展开为没有回路的树形结构(同一路径遇到重复的函数即停止递归)
  let leafCount = 0; // 记录叶子节点数量，用于估算图片尺寸height
  function iter(childrenMap, arr = [], path = new Set()) {
    childrenMap.forEach((func) => {
      const item = {
        functionId: func.toFunc.functionId,
        functionName: func.toFunc.functionName,
        enableStub: func.enableStub,
        covered: func.covered,
        children: [],
        // echarts数据
        itemStyle: {
          borderWidth: 0,
          color: func.covered ? green : gray,
        },
        lineStyle: {
          borderWidth: 0,
          color: func.covered ? green : gray,
        },
        name: func.toFunc.functionId ? `⊱ ${func.toFunc.functionName}` : `? ${func.toFunc.functionName}`,
        label: {
          // eslint-disable-next-line no-nested-ternary
          backgroundColor: func.covered ? green : (func.enableStub ? graybg : whitebg),
          color: func.covered ? fontwhite : fontblack,
          borderColor: 'rgb(164, 177, 190)',
          // eslint-disable-next-line no-nested-ternary
          borderWidth: func.covered ? 0 : (func.enableStub ? 0 : 1),
        },
      };
      if (item.functionId && path.has(item.functionId.toString())) {
        // 回路 bug: a -> b,c && b -> c 由只显示两层规避
        leafCount += 1;
        return;
      }
      if (item.functionId) {
        path.add(item.functionId.toString());
      }
      if (func.toFunc.children) {
        iter(func.toFunc.children, item.children, new Set(path));
      } else {
        leafCount += 1;
      }
      arr.push(item);
    });
    return arr;
  }

  const tree = {
    functionId: funcId,
    functionName: rootFunc.functionName,
    children: [],
    // echarts数据 根节点
    label: {
      position: 'left',
      align: 'left',
      distance: 0,
      backgroundColor: blue,
      color: fontwhite,
    },
    name: rootFunc.functionName,
  };
  if (root && root.children) {
    iter(root.children, tree.children, new Set([funcId]));
  }
  const options = {
    series: [
      {
        type: 'tree',
        data: [tree],
        top: '1%',
        left: '5%',
        bottom: '1%',
        right: '20%',
        symbolSize: 20,
        symbol: 'triangle',
        symbolRotate: -90,
        orient: 'LR',
        label: {
          fontSize: 40,
          position: 'right',
          distance: 0,
          verticalAlign: 'middle',
          align: 'left',
          backgroundColor: green,
          borderRadius: 5,
          padding: 20,
        },
      },
    ],
  };

  // 估算图片尺寸
  const minHeight = 200;
  const height = leafCount * 25 > minHeight ? leafCount * 25 : minHeight;
  const width = 500;

  const outputPath = path.join(outputDir, `${randomString.generate(16)}.png`);
  const DefaultReporter = require('./default-report');
  await utility.writeFile(DefaultReporter.getEChartsImage(options, { width, height }), outputPath);
  return outputPath;
}

module.exports = {
  generateStatemetBlocks,
  generateConditionBlocks,
  generateDecisionBlocks,
  generateBranchBlocks,
  generateMcDcBlocks,
  generateCondMcDcBlocks,
  generateInvokeImage,
};
